<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgl - lightning strike</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="main.css">
	</head>
	<body>

		<div id="container"></div>
		<div id="info"><a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - lightning strike</div>

		<!-- Import maps polyfill -->
		<!-- Remove this when import maps will be widely supported -->
		<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>

		<script type="importmap">
			{
				"imports": {
					"three": "../build/three.module.js",
					"three/addons/": "./jsm/"
				}
			}
		</script>

		<script type="module">

			import * as THREE from 'three';

			import Stats from 'three/addons/libs/stats.module.js';
			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
			import { LightningStrike } from 'three/addons/geometries/LightningStrike.js';
			import { LightningStorm } from 'three/addons/objects/LightningStorm.js';
			import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
			import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
			import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js';

			let container, stats;

			let scene, renderer, composer, gui;

			let currentSceneIndex = 0;

			let currentTime = 0;

			const sceneCreators = [
				createConesScene,
				createPlasmaBallScene,
				createStormScene
			];

			const clock = new THREE.Clock();

			const raycaster = new THREE.Raycaster();
			const mouse = new THREE.Vector2();

			init();
			animate();

			function init() {

				container = document.getElementById( 'container' );

				renderer = new THREE.WebGLRenderer();
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				renderer.outputEncoding = THREE.sRGBEncoding;

				container.appendChild( renderer.domElement );

				composer = new EffectComposer( renderer );

				stats = new Stats();
				container.appendChild( stats.dom );

				window.addEventListener( 'resize', onWindowResize );

				createScene();

			}

			function createScene() {

				scene = sceneCreators[ currentSceneIndex ]();

				createGUI();

			}

			function onWindowResize() {

				scene.userData.camera.aspect = window.innerWidth / window.innerHeight;
				scene.userData.camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );

				composer.setSize( window.innerWidth, window.innerHeight );

			}

			//

			function createGUI() {

				if ( gui ) {

					gui.destroy();

				}

				gui = new GUI( { width: 315 } );

				const sceneFolder = gui.addFolder( 'Scene' );

				scene.userData.sceneIndex = currentSceneIndex;

				sceneFolder.add( scene.userData, 'sceneIndex', { 'Electric Cones': 0, 'Plasma Ball': 1, 'Storm': 2 } ).name( 'Scene' ).onChange( function ( value ) {

					currentSceneIndex = value;

					createScene();

				} );

				scene.userData.timeRate = 1;
				sceneFolder.add( scene.userData, 'timeRate', scene.userData.canGoBackwardsInTime ? - 1 : 0, 1 ).name( 'Time rate' );

				sceneFolder.open();

				const graphicsFolder = gui.addFolder( 'Graphics' );

				graphicsFolder.add( scene.userData, 'outlineEnabled' ).name( 'Glow enabled' );

				scene.userData.lightningColorRGB = [
					scene.userData.lightningColor.r * 255,
					scene.userData.lightningColor.g * 255,
					scene.userData.lightningColor.b * 255
				];
				graphicsFolder.addColor( scene.userData, 'lightningColorRGB' ).name( 'Color' ).onChange( function ( value ) {

					scene.userData.lightningMaterial.color.setRGB( value[ 0 ], value[ 1 ], value[ 2 ] ).multiplyScalar( 1 / 255 );

				} );
				scene.userData.outlineColorRGB = [
					scene.userData.outlineColor.r * 255,
					scene.userData.outlineColor.g * 255,
					scene.userData.outlineColor.b * 255
				];
				graphicsFolder.addColor( scene.userData, 'outlineColorRGB' ).name( 'Glow color' ).onChange( function ( value ) {

					scene.userData.outlineColor.setRGB( value[ 0 ], value[ 1 ], value[ 2 ] ).multiplyScalar( 1 / 255 );

				} );

				graphicsFolder.open();

				const rayFolder = gui.addFolder( 'Ray parameters' );

				rayFolder.add( scene.userData.rayParams, 'straightness', 0, 1 ).name( 'Straightness' );
				rayFolder.add( scene.userData.rayParams, 'roughness', 0, 1 ).name( 'Roughness' );
				rayFolder.add( scene.userData.rayParams, 'radius0', 0.1, 10 ).name( 'Initial radius' );
				rayFolder.add( scene.userData.rayParams, 'radius1', 0.1, 10 ).name( 'Final radius' );
				rayFolder.add( scene.userData.rayParams, 'radius0Factor', 0, 1 ).name( 'Subray initial radius' );
				rayFolder.add( scene.userData.rayParams, 'radius1Factor', 0, 1 ).name( 'Subray final radius' );
				rayFolder.add( scene.userData.rayParams, 'timeScale', 0, 5 ).name( 'Ray time scale' );
				rayFolder.add( scene.userData.rayParams, 'subrayPeriod', 0.1, 10 ).name( 'Subray period (s)' );
				rayFolder.add( scene.userData.rayParams, 'subrayDutyCycle', 0, 1 ).name( 'Subray duty cycle' );

				if ( scene.userData.recreateRay ) {

					// Parameters which need to recreate the ray after modification

					const raySlowFolder = gui.addFolder( 'Ray parameters (slow)' );

					raySlowFolder.add( scene.userData.rayParams, 'ramification', 0, 15 ).step( 1 ).name( 'Ramification' ).onFinishChange( function () {

						scene.userData.recreateRay();

					} );

					raySlowFolder.add( scene.userData.rayParams, 'maxSubrayRecursion', 0, 5 ).step( 1 ).name( 'Recursion' ).onFinishChange( function () {

						scene.userData.recreateRay();

					} );

					raySlowFolder.add( scene.userData.rayParams, 'recursionProbability', 0, 1 ).name( 'Rec. probability' ).onFinishChange( function () {

						scene.userData.recreateRay();

					} );

					raySlowFolder.open();

				}

				rayFolder.open();

			}

			//

			function animate() {

				requestAnimationFrame( animate );

				render();
				stats.update();

			}

			function render() {

				currentTime += scene.userData.timeRate * clock.getDelta();

				if ( currentTime < 0 ) {

					currentTime = 0;

				}

				scene.userData.render( currentTime );

			}

			function createOutline( scene, objectsArray, visibleColor ) {

				const outlinePass = new OutlinePass( new THREE.Vector2( window.innerWidth, window.innerHeight ), scene, scene.userData.camera, objectsArray );
				outlinePass.edgeStrength = 2.5;
				outlinePass.edgeGlow = 0.7;
				outlinePass.edgeThickness = 2.8;
				outlinePass.visibleEdgeColor = visibleColor;
				outlinePass.hiddenEdgeColor.set( 0 );
				composer.addPass( outlinePass );

				scene.userData.outlineEnabled = true;

				return outlinePass;

			}

			//

			function createConesScene() {

				const scene = new THREE.Scene();
				scene.background = new THREE.Color( 0x050505 );

				scene.userData.canGoBackwardsInTime = true;

				scene.userData.camera = new THREE.PerspectiveCamera( 27, window.innerWidth / window.innerHeight, 200, 100000 );

				// Lights

				scene.userData.lightningColor = new THREE.Color( 0xB0FFFF );
				scene.userData.outlineColor = new THREE.Color( 0x00FFFF );

				const posLight = new THREE.PointLight( 0x00ffff, 1, 5000, 2 );
				scene.add( posLight );

				// Ground

				const ground = new THREE.Mesh( new THREE.PlaneGeometry( 200000, 200000 ), new THREE.MeshPhongMaterial( { color: 0xC0C0C0, shininess: 0 } ) );
				ground.rotation.x = - Math.PI * 0.5;
				scene.add( ground );

				// Cones

				const conesDistance = 1000;
				const coneHeight = 200;
				const coneHeightHalf = coneHeight * 0.5;

				posLight.position.set( 0, ( conesDistance + coneHeight ) * 0.5, 0 );
				posLight.color = scene.userData.outlineColor;

				scene.userData.camera.position.set( 5 * coneHeight, 4 * coneHeight, 18 * coneHeight );

				const coneMesh1 = new THREE.Mesh( new THREE.ConeGeometry( coneHeight, coneHeight, 30, 1, false ), new THREE.MeshPhongMaterial( { color: 0xFFFF00, emissive: 0x1F1F00 } ) );
				coneMesh1.rotation.x = Math.PI;
				coneMesh1.position.y = conesDistance + coneHeight;
				scene.add( coneMesh1 );

				const coneMesh2 = new THREE.Mesh( coneMesh1.geometry.clone(), new THREE.MeshPhongMaterial( { color: 0xFF2020, emissive: 0x1F0202 } ) );
				coneMesh2.position.y = coneHeightHalf;
				scene.add( coneMesh2 );

				// Lightning strike

				scene.userData.lightningMaterial = new THREE.MeshBasicMaterial( { color: scene.userData.lightningColor } );

				scene.userData.rayParams = {

					sourceOffset: new THREE.Vector3(),
					destOffset: new THREE.Vector3(),
					radius0: 4,
					radius1: 4,
					minRadius: 2.5,
					maxIterations: 7,
					isEternal: true,

					timeScale: 0.7,

					propagationTimeFactor: 0.05,
					vanishingTimeFactor: 0.95,
					subrayPeriod: 3.5,
					subrayDutyCycle: 0.6,
					maxSubrayRecursion: 3,
					ramification: 7,
					recursionProbability: 0.6,

					roughness: 0.85,
					straightness: 0.6

				};

				let lightningStrike;
				let lightningStrikeMesh;
				const outlineMeshArray = [];

				scene.userData.recreateRay = function () {

					if ( lightningStrikeMesh ) {

						scene.remove( lightningStrikeMesh );

					}

					lightningStrike = new LightningStrike( scene.userData.rayParams );
					lightningStrikeMesh = new THREE.Mesh( lightningStrike, scene.userData.lightningMaterial );

					outlineMeshArray.length = 0;
					outlineMeshArray.push( lightningStrikeMesh );

					scene.add( lightningStrikeMesh );

				};

				scene.userData.recreateRay();

				// Compose rendering

				composer.passes = [];
				composer.addPass( new RenderPass( scene, scene.userData.camera ) );
				createOutline( scene, outlineMeshArray, scene.userData.outlineColor );

				// Controls

				const controls = new OrbitControls( scene.userData.camera, renderer.domElement );
				controls.target.y = ( conesDistance + coneHeight ) * 0.5;
				controls.enableDamping = true;
				controls.dampingFactor = 0.05;

				scene.userData.render = function ( time ) {

					// Move cones and Update ray position
					coneMesh1.position.set( Math.sin( 0.5 * time ) * conesDistance * 0.6, conesDistance + coneHeight, Math.cos( 0.5 * time ) * conesDistance * 0.6 );
					coneMesh2.position.set( Math.sin( 0.9 * time ) * conesDistance, coneHeightHalf, 0 );
					lightningStrike.rayParameters.sourceOffset.copy( coneMesh1.position );
					lightningStrike.rayParameters.sourceOffset.y -= coneHeightHalf;
					lightningStrike.rayParameters.destOffset.copy( coneMesh2.position );
					lightningStrike.rayParameters.destOffset.y += coneHeightHalf;

					lightningStrike.update( time );

					controls.update();

					// Update point light position to the middle of the ray
					posLight.position.lerpVectors( lightningStrike.rayParameters.sourceOffset, lightningStrike.rayParameters.destOffset, 0.5 );

					if ( scene.userData.outlineEnabled ) {

						composer.render();

					}	else {

						renderer.render( scene, scene.userData.camera );

					}

				};

				return scene;

			}

			//

			function createPlasmaBallScene() {

				const scene = new THREE.Scene();

				scene.userData.canGoBackwardsInTime = true;

				scene.userData.camera = new THREE.PerspectiveCamera( 27, window.innerWidth / window.innerHeight, 100, 50000 );

				const ballScene = new THREE.Scene();
				ballScene.background = new THREE.Color( 0x454545 );

				// Lights

				const ambientLight = new THREE.AmbientLight( 0x444444 );
				ballScene.add( ambientLight );
				scene.add( ambientLight );

				const light1 = new THREE.DirectionalLight( 0xffffff, 0.5 );
				light1.position.set( 1, 1, 1 );
				ballScene.add( light1 );
				scene.add( light1 );

				const light2 = new THREE.DirectionalLight( 0xffffff, 1.5 );
				light2.position.set( - 0.5, 1, 0.2 );
				ballScene.add( light2 );
				scene.add( light2 );

				// Plasma ball

				scene.userData.lightningColor = new THREE.Color( 0xFFB0FF );
				scene.userData.outlineColor = new THREE.Color( 0xFF00FF );

				scene.userData.lightningMaterial = new THREE.MeshBasicMaterial( { color: scene.userData.lightningColor, side: THREE.DoubleSide } );

				const r = 'textures/cube/Bridge2/';
				const urls = [ r + 'posx.jpg', r + 'negx.jpg',
							 r + 'posy.jpg', r + 'negy.jpg',
							 r + 'posz.jpg', r + 'negz.jpg' ];

				const textureCube = new THREE.CubeTextureLoader().load( urls );
				textureCube.mapping = THREE.CubeReflectionMapping;
				textureCube.encoding = THREE.sRGBEncoding;

				const sphereMaterial = new THREE.MeshPhysicalMaterial( {
					transparent: true,
					transmission: .96,
					depthWrite: false,
					color: 'white',
					metalness: 0,
					roughness: 0,
					envMap: textureCube
				} );

				const sphereHeight = 300;
				const sphereRadius = 200;

				scene.userData.camera.position.set( 5 * sphereRadius, 2 * sphereHeight, 6 * sphereRadius );

				const sphereMesh = new THREE.Mesh( new THREE.SphereGeometry( sphereRadius, 80, 40 ), sphereMaterial );
				sphereMesh.position.set( 0, sphereHeight, 0 );
				ballScene.add( sphereMesh );

				const sphere = new THREE.Sphere( sphereMesh.position, sphereRadius );

				const spherePlasma = new THREE.Mesh( new THREE.SphereGeometry( sphereRadius * 0.05, 24, 12 ), scene.userData.lightningMaterial );
				spherePlasma.position.copy( sphereMesh.position );
				spherePlasma.scale.y = 0.6;
				scene.add( spherePlasma );

				const post = new THREE.Mesh(
					new THREE.CylinderGeometry( sphereRadius * 0.06, sphereRadius * 0.06, sphereHeight, 6, 1, true ),
					new THREE.MeshLambertMaterial( { color: 0x020202 } )
				);
				post.position.y = sphereHeight * 0.5 - sphereRadius * 0.05 * 1.2;
				scene.add( post );

				const box = new THREE.Mesh( new THREE.BoxGeometry( sphereHeight * 0.5, sphereHeight * 0.1, sphereHeight * 0.5 ), post.material );
				box.position.y = sphereHeight * 0.05 * 0.5;
				scene.add( box );

				const rayDirection = new THREE.Vector3();
				let rayLength = 0;
				const vec1 = new THREE.Vector3();
				const vec2 = new THREE.Vector3();

				scene.userData.rayParams = {

					sourceOffset: sphereMesh.position,
					destOffset: new THREE.Vector3( sphereRadius, 0, 0 ).add( sphereMesh.position ),
					radius0: 4,
					radius1: 4,
					radius0Factor: 0.82,
					minRadius: 2.5,
					maxIterations: 6,
					isEternal: true,

					timeScale: 0.6,
					propagationTimeFactor: 0.15,
					vanishingTimeFactor: 0.87,
					subrayPeriod: 0.8,
					ramification: 5,
					recursionProbability: 0.8,

					roughness: 0.85,
					straightness: 0.7,

					onSubrayCreation: function ( segment, parentSubray, childSubray, lightningStrike ) {

						lightningStrike.subrayConePosition( segment, parentSubray, childSubray, 0.6, 0.9, 0.7 );

						// THREE.Sphere projection

						vec1.subVectors( childSubray.pos1, lightningStrike.rayParameters.sourceOffset );
						vec2.set( 0, 0, 0 );

						if ( lightningStrike.randomGenerator.random() < 0.7 ) {

							vec2.copy( rayDirection ).multiplyScalar( rayLength * 1.0865 );

						}

						vec1.add( vec2 ).setLength( rayLength );
						childSubray.pos1.addVectors( vec1, lightningStrike.rayParameters.sourceOffset );

					}

				};

				let lightningStrike;
				let lightningStrikeMesh;
				const outlineMeshArray = [];

				scene.userData.recreateRay = function () {

					if ( lightningStrikeMesh ) {

						scene.remove( lightningStrikeMesh );

					}

					lightningStrike = new LightningStrike( scene.userData.rayParams );
					lightningStrikeMesh = new THREE.Mesh( lightningStrike, scene.userData.lightningMaterial );

					outlineMeshArray.length = 0;
					outlineMeshArray.push( lightningStrikeMesh );
					outlineMeshArray.push( spherePlasma );

					scene.add( lightningStrikeMesh );

				};

				scene.userData.recreateRay();

				// Compose rendering

				composer.passes = [];

				composer.addPass( new RenderPass( ballScene, scene.userData.camera ) );

				const rayPass = new RenderPass( scene, scene.userData.camera );
				rayPass.clear = false;
				composer.addPass( rayPass );

				const outlinePass = createOutline( scene, outlineMeshArray, scene.userData.outlineColor );

				scene.userData.render = function ( time ) {

					rayDirection.subVectors( lightningStrike.rayParameters.destOffset, lightningStrike.rayParameters.sourceOffset );
					rayLength = rayDirection.length();
					rayDirection.normalize();

					lightningStrike.update( time );

					controls.update();

					outlinePass.enabled = scene.userData.outlineEnabled;

					composer.render();

				};

				// Controls

				const controls = new OrbitControls( scene.userData.camera, renderer.domElement );
				controls.target.copy( sphereMesh.position );
				controls.enableDamping = true;
				controls.dampingFactor = 0.05;

				// THREE.Sphere mouse raycasting

				container.style.touchAction = 'none';
				container.addEventListener( 'pointermove', onPointerMove );

				function onPointerMove( event ) {

					if ( event.isPrimary === false ) return;

					mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
					mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;

					checkIntersection();

				}

				const intersection = new THREE.Vector3();

				function checkIntersection() {

					raycaster.setFromCamera( mouse, scene.userData.camera );

					const result = raycaster.ray.intersectSphere( sphere, intersection );

					if ( result !== null ) {

						lightningStrike.rayParameters.destOffset.copy( intersection );

					}

				}

				return scene;

			}

			//

			function createStormScene() {

				const scene = new THREE.Scene();
				scene.background = new THREE.Color( 0x050505 );

				scene.userData.canGoBackwardsInTime = false;

				scene.userData.camera = new THREE.PerspectiveCamera( 27, window.innerWidth / window.innerHeight, 20, 10000 );

				// Lights

				scene.add( new THREE.AmbientLight( 0x444444 ) );

				const light1 = new THREE.DirectionalLight( 0xffffff, 0.5 );
				light1.position.set( 1, 1, 1 );
				scene.add( light1 );

				const posLight = new THREE.PointLight( 0x00ffff );
				posLight.position.set( 0, 100, 0 );
				scene.add( posLight );

				// Ground

				const GROUND_SIZE = 1000;

				scene.userData.camera.position.set( 0, 0.2, 1.6 ).multiplyScalar( GROUND_SIZE * 0.5 );

				const ground = new THREE.Mesh( new THREE.PlaneGeometry( GROUND_SIZE, GROUND_SIZE ), new THREE.MeshLambertMaterial( { color: 0x072302 } ) );
				ground.rotation.x = - Math.PI * 0.5;
				scene.add( ground );

				// Storm

				scene.userData.lightningColor = new THREE.Color( 0xB0FFFF );
				scene.userData.outlineColor = new THREE.Color( 0x00FFFF );

				scene.userData.lightningMaterial = new THREE.MeshBasicMaterial( { color: scene.userData.lightningColor } );

				const rayDirection = new THREE.Vector3( 0, - 1, 0 );
				let rayLength = 0;
				const vec1 = new THREE.Vector3();
				const vec2 = new THREE.Vector3();

				scene.userData.rayParams = {

					radius0: 1,
					radius1: 0.5,
					minRadius: 0.3,
					maxIterations: 7,

					timeScale: 0.15,
					propagationTimeFactor: 0.2,
					vanishingTimeFactor: 0.9,
					subrayPeriod: 4,
					subrayDutyCycle: 0.6,

					maxSubrayRecursion: 3,
					ramification: 3,
					recursionProbability: 0.4,

					roughness: 0.85,
					straightness: 0.65,

					onSubrayCreation: function ( segment, parentSubray, childSubray, lightningStrike ) {

						lightningStrike.subrayConePosition( segment, parentSubray, childSubray, 0.6, 0.6, 0.5 );

						// Plane projection

						rayLength = lightningStrike.rayParameters.sourceOffset.y;
						vec1.subVectors( childSubray.pos1, lightningStrike.rayParameters.sourceOffset );
						const proj = rayDirection.dot( vec1 );
						vec2.copy( rayDirection ).multiplyScalar( proj );
						vec1.sub( vec2 );
						const scale = proj / rayLength > 0.5 ? rayLength / proj : 1;
						vec2.multiplyScalar( scale );
						vec1.add( vec2 );
						childSubray.pos1.addVectors( vec1, lightningStrike.rayParameters.sourceOffset );

					}

				};

				// Black star mark
				const starVertices = [];
				const prevPoint = new THREE.Vector3( 0, 0, 1 );
				const currPoint = new THREE.Vector3();
				for ( let i = 1; i <= 16; i ++ ) {

					currPoint.set( Math.sin( 2 * Math.PI * i / 16 ), 0, Math.cos( 2 * Math.PI * i / 16 ) );

					if ( i % 2 === 1 ) {

						currPoint.multiplyScalar( 0.3 );

					}

					starVertices.push( 0, 0, 0 );
					starVertices.push( prevPoint.x, prevPoint.y, prevPoint.z );
					starVertices.push( currPoint.x, currPoint.y, currPoint.z );

					prevPoint.copy( currPoint );

				}

				const starGeometry = new THREE.BufferGeometry();
				starGeometry.setAttribute( 'position', new THREE.Float32BufferAttribute( starVertices, 3 ) );
				const starMesh = new THREE.Mesh( starGeometry, new THREE.MeshBasicMaterial( { color: 0x020900 } ) );
				starMesh.scale.multiplyScalar( 6 );

				//

				const storm = new LightningStorm( {

					size: GROUND_SIZE,
					minHeight: 90,
					maxHeight: 200,
					maxSlope: 0.6,
					maxLightnings: 8,

					lightningParameters: scene.userData.rayParams,

					lightningMaterial: scene.userData.lightningMaterial,

					onLightningDown: function ( lightning ) {

						// Add black star mark at ray strike
						const star1 = starMesh.clone();
						star1.position.copy( lightning.rayParameters.destOffset );
						star1.position.y = 0.05;
						star1.rotation.y = 2 * Math.PI * Math.random();
						scene.add( star1 );

					}

				} );

				scene.add( storm );

				// Compose rendering

				composer.passes = [];
				composer.addPass( new RenderPass( scene, scene.userData.camera ) );
				createOutline( scene, storm.lightningsMeshes, scene.userData.outlineColor );

				// Controls

				const controls = new OrbitControls( scene.userData.camera, renderer.domElement );
				controls.target.y = GROUND_SIZE * 0.05;
				controls.enableDamping = true;
				controls.dampingFactor = 0.05;

				scene.userData.render = function ( time ) {

					storm.update( time );

					controls.update();

					if ( scene.userData.outlineEnabled ) {

						composer.render();

					}	else {

						renderer.render( scene, scene.userData.camera );

					}

				};

				return scene;

			}

		</script>

	</body>
</html>
